﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Media.Media3D;
using System.Windows.Media;
using System.Diagnostics;
using System.ComponentModel;

namespace SkeletalAnimation3D.Core
{
    public class MeshInterpolator : InterpolatorBase
    {
        private enum InterpolationState
        {
            Mesh,
            Frame,
            Interpolation
        }

        private InterpolationState _interpolationState = InterpolationState.Mesh;
        private int _currentFrame = -1;
        private MeshInfo _currentMeshCache;
        private MeshInfo _meshCache1;
        private MeshInfo _meshCache2;
        private SkeletonInfo _currentSkeletonCache;
        private SkeletonInfo _skeletonCache1;
        private SkeletonInfo _skeletonCache2;
        private BoneAnimationLibrary _animationLibrary;
        private BoneAnimation _currentAnimation;

        public MeshInterpolator(BoneAnimationLibrary animationLibrary)
        {
            _animationLibrary = animationLibrary;
            
            _meshCache1 = _animationLibrary.GetMeshInfo();
            _meshCache2 = _animationLibrary.GetMeshInfo();
            _skeletonCache1 = _animationLibrary.GetSkeletonInfo();
            _skeletonCache2 = _animationLibrary.GetSkeletonInfo();
            _animationLibrary.PropertyChanged += new PropertyChangedEventHandler(AnimationLibrary_PropertyChanged);
        }

        void AnimationLibrary_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Skeleton")
            {
                _skeletonCache1 = _animationLibrary.GetSkeletonInfo();
                _skeletonCache2 = _animationLibrary.GetSkeletonInfo();
            }
            else if (e.PropertyName == "Mesh")
            {
                _meshCache1 = _animationLibrary.GetMeshInfo();
                _meshCache2 = _animationLibrary.GetMeshInfo();
            }

            switch (_interpolationState)
            {
                case InterpolationState.Mesh:
                    ShowMesh();
                    break;
                case InterpolationState.Frame:
                    ShowAnimationFrame(_currentAnimation, _currentFrame);
                    break;
                case InterpolationState.Interpolation:
                    ShowAnimationStill(_currentAnimation, AnimationTimeOffset);
                    break;
                default:
                    break;
            }

        }

        public MeshInfo Mesh
        {
            get { return (MeshInfo)GetValue(MeshProperty); }
            set { SetValue(MeshProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Mesh.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MeshProperty =
            DependencyProperty.Register("Mesh", typeof(MeshInfo), typeof(MeshInterpolator), new UIPropertyMetadata(null));



        public SkeletonInfo Skeleton
        {
            get { return (SkeletonInfo)GetValue(SkeletonProperty); }
            set { SetValue(SkeletonProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Skeleton.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SkeletonProperty =
            DependencyProperty.Register("Skeleton", typeof(SkeletonInfo), typeof(MeshInterpolator), new UIPropertyMetadata(null));

        public double AnimationTimeOffset
        {
            get { return (double)GetValue(AnimationTimeOffsetProperty); }
            set { SetValue(AnimationTimeOffsetProperty, value); }
        }

        // Using a DependencyProperty as the backing store for AnimationTimeOffset.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AnimationTimeOffsetProperty =
            DependencyProperty.Register("AnimationTimeOffset", typeof(double), typeof(MeshInterpolator), new UIPropertyMetadata(0.0));

        public override void ShowMesh()
        {
            _currentMeshCache = (_currentMeshCache == null || _currentMeshCache == _meshCache2) ? _meshCache1 : _meshCache2;
            _currentSkeletonCache = (_currentSkeletonCache == null || _currentSkeletonCache == _skeletonCache2) ? _skeletonCache1 : _skeletonCache2;

            _animationLibrary.CopyMeshData(_currentMeshCache);
            _animationLibrary.CopySkeletonData(_currentSkeletonCache);

            Mesh = _currentMeshCache;
            Skeleton = _currentSkeletonCache;
            AnimationTimeOffset = 0;
            _interpolationState = InterpolationState.Mesh;
        }

        public override void ShowAnimationFrame(BoneAnimation animation, int frame)
        {
            _currentAnimation = animation;
            _currentMeshCache = (_currentMeshCache == null || _currentMeshCache == _meshCache2) ? _meshCache1 : _meshCache2;
            _currentSkeletonCache = (_currentSkeletonCache == null || _currentSkeletonCache == _skeletonCache2) ? _skeletonCache1 : _skeletonCache2;

            _animationLibrary.CopyMeshData(_currentMeshCache);
            _animationLibrary.CopySkeletonData(_currentSkeletonCache);
            _currentAnimation.Frames[frame].CopySkeletonOrientation(_currentSkeletonCache);
            
            ApplyBoneTransformations();

            Skeleton = _currentSkeletonCache;
            Mesh = _currentMeshCache;
            AnimationTimeOffset = _currentAnimation.Frames[frame].Offset;
            _interpolationState = InterpolationState.Frame;
            _currentFrame = frame;
        }

        public override void ShowAnimationStill(BoneAnimation animation, double progress)
        {
            _currentAnimation = animation;
            _currentMeshCache = (_currentMeshCache == null || _currentMeshCache == _meshCache2) ? _meshCache1 : _meshCache2; 
            _currentSkeletonCache = (_currentSkeletonCache == null || _currentSkeletonCache == _skeletonCache2) ? _skeletonCache1 : _skeletonCache2;

            BoneFrame frame1;
            BoneFrame frame2;
            double deltaProgress;
            _currentAnimation.FindSurroundingFrames(progress, out frame1, out frame2);
            deltaProgress = (progress - frame1.Offset) / (frame2.Offset - frame1.Offset);

            _animationLibrary.CopyMeshData(_currentMeshCache);
            _animationLibrary.CopySkeletonData(_currentSkeletonCache);
            BoneOrientationDictionary.InterpolateBoneTransformations(frame1.BoneOrientations, frame2.BoneOrientations, deltaProgress, _currentSkeletonCache);
            ApplyBoneTransformations();

            Skeleton = _currentSkeletonCache;
            Mesh = _currentMeshCache;
            AnimationTimeOffset = progress;
            _interpolationState = InterpolationState.Interpolation;
        }

        public override void Interpolate(double progress)
        {
            _currentMeshCache = (_currentMeshCache == null || _currentMeshCache == _meshCache2) ? _meshCache1 : _meshCache2;
            _currentSkeletonCache = (_currentSkeletonCache == null || _currentSkeletonCache == _skeletonCache2) ? _skeletonCache1 : _skeletonCache2;

            int frames = _currentAnimation.Frames.Count;
            if (frames == 0)
            {
                //use mesh as animation
                _animationLibrary.CopyMeshData(_currentMeshCache);
                _animationLibrary.CopySkeletonData(_currentSkeletonCache);

            }
            else if (frames == 1)
            {
                _animationLibrary.CopyMeshData(_currentMeshCache);
                _animationLibrary.CopySkeletonData(_currentSkeletonCache);
                _currentAnimation.Frames[0].CopySkeletonOrientation(_currentSkeletonCache);
                ApplyBoneTransformations();
            }
            else
            {
                BoneFrame frame1;
                BoneFrame frame2;
                double deltaProgress;
                _currentAnimation.FindSurroundingFrames(progress, out frame1, out frame2);
                deltaProgress = (progress - frame1.Offset) / (frame2.Offset - frame1.Offset);

                _animationLibrary.CopyMeshData(_currentMeshCache);
                _animationLibrary.CopySkeletonData(_currentSkeletonCache);
                BoneOrientationDictionary.InterpolateBoneTransformations(frame1.BoneOrientations, frame2.BoneOrientations, deltaProgress, _currentSkeletonCache);
                ApplyBoneTransformations();
            }

            Skeleton = _currentSkeletonCache;
            Mesh = _currentMeshCache;
            AnimationTimeOffset = progress;
            _interpolationState = InterpolationState.Interpolation;
        }

        
        private void ApplyBoneTransformations()
        {
            //skip the rootbone as that should be used on the entire mesh
            foreach (BoneInfo boneInfo in _currentSkeletonCache.Root.Children)
            {
                ApplyBoneTransformation(boneInfo);
            }
        }

        private void ApplyBoneTransformation(BoneInfo boneInfo)
        {
            TransformMesh(boneInfo);

            if (boneInfo.Children != null)
            {
                foreach (BoneInfo childBone in boneInfo.Children)
                {
                    ApplyBoneTransformation(childBone);
                }
            }
        }

        private void TransformMesh(BoneInfo boneInfo)
        {
            //transform attached points and normals according to their weights
            Int32Collection pointIndices = _animationLibrary.Skeleton.GetAttachedPoints(boneInfo.Id);
            DoubleCollection pointWeights = _animationLibrary.Skeleton.GetPointWeights(boneInfo.Id);
            for (int p = 0; p < pointIndices.Count; p++)
            {
                int index = pointIndices[p];
                Quaternion weightedRotation = new Quaternion(boneInfo.Orientation.Axis, boneInfo.Orientation.Angle * pointWeights[p]);
                Matrix3D weightedRotationMatrix = Matrix3D.Identity;
                weightedRotationMatrix.Rotate(weightedRotation);
                Point3D pointCopy = _currentMeshCache.Points[index];
                pointCopy.Offset(-boneInfo.Position.X, -boneInfo.Position.Y, -boneInfo.Position.Z);
                pointCopy = Point3D.Multiply(pointCopy, weightedRotationMatrix);
                pointCopy.Offset(boneInfo.Position.X, boneInfo.Position.Y, boneInfo.Position.Z);
                _currentMeshCache.Points[index] = pointCopy;
                Vector3D vectorCopy = _currentMeshCache.Normals[index];
                vectorCopy = Vector3D.Multiply(vectorCopy, weightedRotationMatrix);
                _currentMeshCache.Normals[index] = vectorCopy;

            }
            
            //transform child bones
            if (boneInfo.Children != null && boneInfo.Children.Count > 0)
            {
                Matrix3D rotationMatrix = Matrix3D.Identity;
                rotationMatrix.Rotate(boneInfo.Orientation);
                foreach (BoneInfo childBone in boneInfo.Children)
                {
                    //rotate bone position
                    Point3D copy = childBone.Position;
                    copy.Offset(-boneInfo.Position.X, -boneInfo.Position.Y, -boneInfo.Position.Z);
                    copy = Point3D.Multiply(copy, rotationMatrix);
                    copy.Offset(boneInfo.Position.X, boneInfo.Position.Y, boneInfo.Position.Z);
                    childBone.Position = copy;

                    //rotate bone orientation
                    Quaternion q = childBone.Orientation;
                    Vector3D newAxis = Vector3D.Multiply(q.Axis, rotationMatrix);
                    childBone.Orientation = new Quaternion(newAxis, q.Angle);

                    // repeat for children
                    TransformMeshChildren(childBone, boneInfo, rotationMatrix);
                }
            }
        }

        private void TransformMeshChildren(BoneInfo boneInfo, BoneInfo pivotBone, Matrix3D rotationMatrix)
        {
            //transform points and normals
            Int32Collection pointIndices = _animationLibrary.Skeleton.GetAttachedPoints(boneInfo.Id);
            for (int p = 0; p < pointIndices.Count; p++)
            {
                int index = pointIndices[p];
                Point3D pointCopy = _currentMeshCache.Points[index];
                pointCopy.Offset(-pivotBone.Position.X, -pivotBone.Position.Y, -pivotBone.Position.Z);
                pointCopy = Point3D.Multiply(pointCopy, rotationMatrix);
                pointCopy.Offset(pivotBone.Position.X, pivotBone.Position.Y, pivotBone.Position.Z);
                _currentMeshCache.Points[index] = pointCopy;
                Vector3D vectorCopy = _currentMeshCache.Normals[index];
                vectorCopy = Vector3D.Multiply(vectorCopy, rotationMatrix);
                _currentMeshCache.Normals[index] = vectorCopy;

            }
            //transform child bones recursivly
            if (boneInfo.Children != null && boneInfo.Children.Count > 0)
            {
                foreach (BoneInfo childBone in boneInfo.Children)
                {
                    //rotate bone position
                    Point3D copy = childBone.Position;
                    copy.Offset(-pivotBone.Position.X, -pivotBone.Position.Y, -pivotBone.Position.Z);
                    copy = Point3D.Multiply(copy, rotationMatrix);
                    copy.Offset(pivotBone.Position.X, pivotBone.Position.Y, pivotBone.Position.Z);
                    childBone.Position = copy;

                    //rotate bone orientation
                    Quaternion q = childBone.Orientation;
                    Vector3D newAxis = Vector3D.Multiply(q.Axis, rotationMatrix);
                    childBone.Orientation = new Quaternion(newAxis, q.Angle);

                    // repeat for children
                    TransformMeshChildren(childBone, pivotBone, rotationMatrix);
                }
            }
        }
    }
}
